"""Helper for creating requests Session."""
from collections import abc
from importlib import metadata
import logging
import os.path
import typing
from urllib import parse

import prometheus_client as prometheus
import requests
from requests import adapters
from urllib3.util.retry import Retry

from .logger import get_logger

FEDORA_BUNDLE = '/etc/pki/tls/certs/ca-bundle.crt'
DEBIAN_BUNDLE = '/etc/ssl/certs/ca-certificates.crt'

METRIC_REQUEST_DURATION_SECONDS = prometheus.Histogram(
    'cki_http_request_duration_seconds',
    'Time spent making a request',
    ['hostname']
)
METRIC_REQUESTS_BY_STATUS = prometheus.Counter(
    'cki_http_requests_by_status',
    'Number of requests by status',
    ['hostname', 'status']
)


class RequestLogger:
    """Add logging to a requests.Session."""

    def __init__(self, logger: logging.Logger) -> None:
        """Create a requests.Session logger instance."""
        self.logger = logger

    def hook(self, session: requests.Session) -> None:
        """Add logging to a requests.Session."""
        session.hooks['response'].append(self.log_request)

    def log_request(self, response: requests.Response, *_: typing.Any, **__: typing.Any) -> None:
        """Log a request via logging and metrics."""
        request_duration = response.elapsed.total_seconds()
        self.logger.debug(
            '[%.3fs] %s %s [%s]',
            request_duration, response.request.method, response.url, response.status_code)

        hostname = parse.urlsplit(response.url).netloc
        METRIC_REQUEST_DURATION_SECONDS.labels(hostname).observe(request_duration)
        METRIC_REQUESTS_BY_STATUS.labels(hostname, response.status_code).inc()


def get_session(
    user_agent: str,
    *,
    headers: dict[str, str] | None = None,
    logger: logging.Logger | None = None,
    retry_args: dict[str, typing.Any] | None = None,
    raise_for_status: bool = False,
    timeout: float = 300,
) -> requests.Session:
    # pylint: disable=too-many-arguments,too-many-positional-arguments
    """Return pre-configured requests Session.

    The session contains configured user_agent, retries
    and optional logging.

    By default, the requests are logged with DEBUG priority, either by the
    explicitly specified logger or a logger instantiated with the same name as
    the user agent. This can be disabled by passing logger=None.
    """
    session = requests.Session()
    session.headers.update({'User-Agent': user_agent})
    session.headers.update(headers or {})

    RequestLogger(logger or get_logger(user_agent)).hook(session)

    if raise_for_status:
        def do_raising(response: requests.Response, *_: typing.Any, **__: typing.Any) -> None:
            response.raise_for_status()
        session.hooks['response'].append(do_raising)

    default_retry_args: dict[str, typing.Any] = {'total': 5, 'backoff_factor': 1}
    default_retry_args.update(retry_args or {})
    # workaround for urllib3<2, e.g. as used in Fedora at least up to FC40
    if 'backoff_max' in default_retry_args and metadata.version('urllib3').startswith('1.'):
        backoff_max = default_retry_args.pop('backoff_max')
        retry = Retry(**default_retry_args)
        retry.DEFAULT_BACKOFF_MAX = backoff_max
    else:
        retry = Retry(**default_retry_args)
    adapter = _TimeoutHTTPAdapter(timeout=timeout, max_retries=retry)
    session.mount('http://', adapter)
    session.mount('https://', adapter)

    # By default, requests uses certificates from REQUESTS_CA_BUNDLE,
    # CURL_CA_BUNDLE, or the certifi package. Distributions divert the certifi
    # default to
    #   Fedora: /etc/pki/tls/certs/ca-bundle.crt
    #   Debian: /etc/ssl/certs/ca-certificates.crt
    # With a pip-installed requests, this is not the case. In that case, any
    # certs added to the system certificate bundle are ignored unless
    # REQUESTS_CA_BUNDLE is explicitly set. Do The Right Thing in this case and
    # use any of the distribution bundles if found.

    bundle = (os.environ.get('REQUESTS_CA_BUNDLE') or
              os.environ.get('CURL_CA_BUNDLE') or
              (os.path.isfile(FEDORA_BUNDLE) and FEDORA_BUNDLE) or
              (os.path.isfile(DEBIAN_BUNDLE) and DEBIAN_BUNDLE))
    if bundle:
        session.verify = bundle

    return session


class _TimeoutHTTPAdapter(adapters.HTTPAdapter):
    def __init__(self, timeout: float, **kwargs: typing.Any) -> None:
        super().__init__(**kwargs)
        self.timeout = timeout

    def send(
        self,
        request: requests.PreparedRequest,
        stream: bool = False,
        timeout: None | float | tuple[float, float] | tuple[float, None] = None,
        verify: bool | str = True,
        cert: None | bytes | str | tuple[bytes | str, bytes | str] = None,
        proxies: abc.Mapping[str, str] | None = None,
    ) -> requests.Response:
        # pylint: disable=too-many-arguments,too-many-positional-arguments
        return super().send(request, stream=stream, timeout=timeout or self.timeout,
                            verify=verify, cert=cert, proxies=proxies)
